The Road to React
https://gyazo.com/cea27a9b77d55891f98efe977604d626
Hello React
読んでおきたいサイト
Node and NPM
code:sh
$ node --version
v18.10.0
$ npm --version
8.19.2
Setting up a React Project
code:sh
cd src
npm create vite@latest hacker-stories -- --template react-ts
cd hacker-stories
npm install
npm run dev
https://gyazo.com/7819db4d58f8c6ee1079cc624fae7438
リリースビルド
npm run build でリリース向けのビルドを作成できる。
code:sh
npm run build
code:sh
ruby -rwebrick -e 'WEBrick::HTTPServer.new(:DocumentRoot => "./dist", :Port => 8000).start'
Vite
Meet the React Component
code:src/App.tsx
import * as React from 'react'
function App() {
return (
<div>
<h1>Hello React with TypeScript</h1>
</div>
)
}
export default App
React JSX
code:src/App.tsx.diff
diff --git a/src/App.tsx b/src/App.tsx
index 0cebb1b..45a930e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,9 +1,11 @@
import * as React from 'react'
+const title = "React"
+
function App() {
return (
<div>
- <h1>Hello React</h1>
+ <h1>Hello {title}!!</h1>
</div>
)
}
All Supported HTML Attributes
Lists in React
code:src/App.tsx.diff
diff --git a/src/App.tsx b/src/App.tsx
index 3cbbe5f..c43d370 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,18 +1,48 @@
import * as React from 'react'
-const welcome = {
- greeting: 'Hey',
- title: 'React',
-};
+const list = [
+ {
+ title: 'React',
+ author: 'Jordan Walke',
+ num_comments: 3,
+ points: 4,
+ objectID: 0,
+ },
+ {
+ title: 'Redux',
+ author: 'Dan Abramov, Andrew Clark',
+ num_comments: 2,
+ points: 5,
+ objectID: 1,
+ },
+]
+
function App() {
return (
<div>
<h1>
- {welcome.greeting} {welcome.title}
+ My Hacker Stories
</h1>
<label htmlFor="search">Search: </label>
<input id="search" type="text" />
+
+ <hr />
+
+ <ul>
+ {list.map(function (item) {
+ return (
+ <li key={item.objectID}>
+ <span><a href={item.url}>{item.title}</a></span>
+ <span>{item.author}</span>
+ <span>{item.num_comments}</span>
+ <span>{item.points}</span>
+ </li>
+ )
+ })}
+ </ul>
</div>
)
}
https://gyazo.com/6b999ca8a0ebc51040c079e53384cafe
Meet another React Component
リストと検索の部分をコンポーネント化してみる。
code:src/App.tsx.diff
diff --git a/src/App.tsx b/src/App.tsx
index c43d370..04069c0 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -26,25 +26,37 @@ function App() {
<h1>
My Hacker Stories
</h1>
- <label htmlFor="search">Search: </label>
- <input id="search" type="text" />
-
+ <Search />
<hr />
+ <List />
+ </div>
+ )
+}
- <ul>
- {list.map(function (item) {
- return (
- <li key={item.objectID}>
- <span><a href={item.url}>{item.title}</a></span>
- <span>{item.author}</span>
- <span>{item.num_comments}</span>
- <span>{item.points}</span>
- </li>
- )
- })}
- </ul>
+function Search() {
+ return (
+ <div>
+ <label htmlFor="search">Search: </label>
+ <input id="search" type="text" />
</div>
)
}
+function List() {
+ return (
+ <ul>
+ {list.map(function (item) {
+ return (
+ <li key={item.objectID}>
+ <span><a href={item.url}>{item.title}</a></span>
+ <span>{item.author}</span>
+ <span>{item.num_comments}</span>
+ <span>{item.points}</span>
+ </li>
+ )
+ })}
+ </ul>
+ )
+}
+
export default App
React DOM
code:main.tsx
import ReactDOM from 'react-dom/client'
const title = "React"
ReactDOM.createRoot(document.getElementById("root")).render(
<>
<h1>Hello {title} {title}</h1>
<h1>Hello {title} {title}</h1>
</>
)
https://gyazo.com/e314f326eec69d02b22cf34f739ba6d6
React's StrictMode
アロー関数
code:javascript
const double = (x) => { return x * 2 }
//=> undefined
double(10)
//=> 20
React Component Declaration
code:src/App.tsx.diff
diff --git a/src/App.tsx b/src/App.tsx
index 04069c0..42a8bc5 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -19,44 +19,35 @@ const list = [
},
]
+const App = () => (
+ <div>
+ <h1>
+ My Hacker Stories
+ </h1>
+ <Search />
+ <hr />
+ <List />
+ </div>
+)
-function App() {
- return (
- <div>
- <h1>
- My Hacker Stories
- </h1>
- <Search />
- <hr />
- <List />
- </div>
- )
-}
+const Search = () => (
+ <div>
+ <label htmlFor="search">Search: </label>
+ <input id="search" type="text" />
+ </div>
+)
-function Search() {
- return (
- <div>
- <label htmlFor="search">Search: </label>
- <input id="search" type="text" />
- </div>
- )
-}
-
-function List() {
- return (
- <ul>
- {list.map(function (item) {
- return (
- <li key={item.objectID}>
- <span><a href={item.url}>{item.title}</a></span>
- <span>{item.author}</span>
- <span>{item.num_comments}</span>
- <span>{item.points}</span>
- </li>
- )
- })}
- </ul>
- )
-}
+const List = () => (
+ <ul>
+ {list.map((item) => (
+ <li key={item.objectID}>
+ <span><a href={item.url}>{item.title}</a></span>
+ <span>{item.author}</span>
+ <span>{item.num_comments}</span>
+ <span>{item.points}</span>
+ </li>
+ ))}
+ </ul>
+)
export default App
Handler Function in JSX
テキストフィールドの変更をイベントハンドラで受け取るようにする。
code:src/App.tsx
diff --git a/src/App.tsx b/src/App.tsx
index 60cdf28..eb41cdc 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -31,10 +31,15 @@ const App = () => (
)
const Search = () => {
+ const handleChange = (event: React.BaseSyntheticEvent) => {
+ console.log(event)
+ console.log(event.target.value)
+ }
+
return (
<div>
<label htmlFor="search">Search: </label>
- <input id="search" type="text" />
+ <input id="search" type="text" onChange={handleChange}/>
</div>
)
}
テキストフィールドの値が変更されるたびにコンソールにイベントと値が表示される。
https://gyazo.com/8d0bd7c83be7b183c48a844b0c2870de
React Event Handler
React Props
上位のコンポーネントから下位のコンポーネントへプロパティを渡す。
code:src/App.tsx
import * as React from 'react'
type Story = {
title: string
url: string
author: string
num_comments: number
points: number
objectID: number
}
const stories: Story[] = [
{
title: 'React',
author: 'Jordan Walke',
num_comments: 3,
points: 4,
objectID: 0,
},
{
title: 'Redux',
author: 'Dan Abramov, Andrew Clark',
num_comments: 2,
points: 5,
objectID: 1,
},
]
const App = () => (
<div>
<h1>
My Hacker Stories
</h1>
<Search />
<hr />
<List list={stories} />
</div>
)
const Search = () => {
const handleChange = (event: React.BaseSyntheticEvent) => {
// イベントを出力
console.log(event)
// テキストフィールドの値を出力
console.log(event.target.value)
}
return (
<div>
<label htmlFor="search">Search: </label>
<input id="search" type="text" onChange={handleChange}/>
</div>
)
}
type ItemProps = {
item: Story
}
const Item = (props: ItemProps) => (
<li key={props.item.objectID}>
<span><a href={props.item.url}>{props.item.title}</a></span>
<span>{props.item.author}</span>
<span>{props.item.num_comments}</span>
<span>{props.item.points}</span>
</li>
)
type ListProps = {
list: Story[]
}
const List = (props: ListProps) => (
<ul>
{props.list.map((item: any) => (
<Item key={item.objectID} item={item} />
))}
</ul>
)
export default App
React State
useState を使って検索キーワードをstateとして管理するようにしたことで、テキストフィールドに入れた文字列が Searching for の後ろに動的に表示されるようにできる。
https://gyazo.com/ec8197889c28cc961704948ba597b29b
code:src/App.tsx
diff --git a/src/App.tsx b/src/App.tsx
index 1bfa1a8..adab51c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -40,13 +40,17 @@ const App = () => (
)
const Search = () => {
+
const handleChange = (event: React.BaseSyntheticEvent) => {
+ setSearchTerm(event.target.value)
}
return (
<div>
<label htmlFor="search">Search: </label>
<input id="search" type="text" onChange={handleChange}/>
+ <p>Searching for <strong>{searchTerm}</strong></p>
</div>
)
}
useState は React hook
Callback Handlers in JSX
検索キーワード変更に応じてリストを絞り込むためには、Searchコンポーネントへ上位のコンポーネントで定義したハンドラを渡す必要がある。
code:src/App.tsx.diff
diff --git a/src/App.tsx b/src/App.tsx
index 191d01f..1fc6ddf 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -29,23 +29,32 @@ const stories: Story[] = [
]
const App = () => {
+ const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
+ console.log(event)
+ }
+
return (
<div>
<h1>
My Hacker Stories
</h1>
- <Search />
+ <Search onSearch={handleSearch}/>
<hr />
<List list={stories} />
</div>
)
}
-const Search = () => {
+type SearchProps = {
+ onSearch: (event: React.ChangeEvent<HTMLInputElement>) => void
+}
+
+const Search = (props: SearchProps) => {
- const handleChange = (event: React.BaseSyntheticEvent) => {
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value)
+ props.onSearch(event)
}
return (
Lifting State in React
ステートをSearchコンポーネントからAppコンポーネントへ移動。
code:src/App.tsx.diff
diff --git a/src/App.tsx b/src/App.tsx
index 53b8edd..7c9ae53 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -29,7 +29,10 @@ const stories: Story[] = [
]
const App = () => {
+
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
+ setSearchTerm(event.target.value)
console.log(event)
}
@@ -50,18 +53,10 @@ type SearchProps = {
}
const Search = (props: SearchProps) => {
-
- const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- setSearchTerm(event.target.value)
- props.onSearch(event)
- }
-
return (
<div>
<label htmlFor="search">Search: </label>
- <input id="search" type="text" onChange={handleChange}/>
- <p>Searching for <strong>{searchTerm}</strong></p>
+ <input id="search" type="text" onChange={props.onSearch}/>
</div>
)
}
入力した文字列で絞り込めるようにする。
code:src/App.tsx.diff
diff --git a/src/App.tsx b/src/App.tsx
index 7c9ae53..a7bbf85 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -9,33 +9,36 @@ type Story = {
objectID: number
}
-const stories: Story[] = [
- {
- title: 'React',
- author: 'Jordan Walke',
- num_comments: 3,
- points: 4,
- objectID: 0,
- },
- {
- title: 'Redux',
- author: 'Dan Abramov, Andrew Clark',
- num_comments: 2,
- points: 5,
- objectID: 1,
- },
-]
-
const App = () => {
+ console.log("Redner App !!!")
+
+ const stories: Story[] = [
+ {
+ title: 'React',
+ author: 'Jordan Walke',
+ num_comments: 3,
+ points: 4,
+ objectID: 0,
+ },
+ {
+ title: 'Redux',
+ author: 'Dan Abramov, Andrew Clark',
+ num_comments: 2,
+ points: 5,
+ objectID: 1,
+ },
+ ]
+
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value)
- console.log(event)
}
+ const searchStories = stories.filter((story) => { return story.title.includes(searchTerm) })
+
return (
<div>
<h1>
@@ -43,7 +46,7 @@ const App = () => {
</h1>
<Search onSearch={handleSearch}/>
<hr />
- <List list={stories} />
+ <List list={searchStories} />
</div>
)
}
大文字・小文字を区別しないようにする。
code:src/App.tsx.diff
diff --git a/src/App.tsx b/src/App.tsx
index a7bbf85..dd0d176 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -37,7 +37,7 @@ const App = () => {
setSearchTerm(event.target.value)
}
- const searchStories = stories.filter((story) => { return story.title.includes(searchTerm) })
+ const searchStories = stories.filter((story) => { return story.title.toLowerCase().includes(searchTerm.toLowerCase()) })
return (
<div>
React Controlled Components
これまではテキストフィールドへ入力した値がReactのステートへ反映されるだけだったので、Reactのステートの値がテキストフィールドへ表示されるように修正。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index dd0d176..9cc4989 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -31,7 +31,7 @@ const App = () => {
},
]
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value)
@@ -44,7 +44,7 @@ const App = () => {
<h1>
My Hacker Stories
</h1>
- <Search onSearch={handleSearch}/>
+ <Search search={searchTerm} onSearch={handleSearch}/>
<hr />
<List list={searchStories} />
</div>
@@ -52,6 +52,7 @@ const App = () => {
}
type SearchProps = {
+ search: string
onSearch: (event: React.ChangeEvent<HTMLInputElement>) => void
}
@@ -59,7 +60,12 @@ const Search = (props: SearchProps) => {
return (
<div>
<label htmlFor="search">Search: </label>
- <input id="search" type="text" onChange={props.onSearch}/>
+ <input
+ id="search"
+ type="text"
+ value={props.search}
+ onChange={props.onSearch}
+ />
</div>
)
}
Props Handling (Advanced)
分割代入 (Destructuring assignment) を使って props まわりをスッキリ書くことができる。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index 9cc4989..4cae712 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -56,15 +56,15 @@ type SearchProps = {
onSearch: (event: React.ChangeEvent<HTMLInputElement>) => void
}
-const Search = (props: SearchProps) => {
+const Search = ({search, onSearch}: SearchProps) => {
return (
<div>
<label htmlFor="search">Search: </label>
<input
id="search"
type="text"
- value={props.search}
- onChange={props.onSearch}
+ value={search}
+ onChange={onSearch}
/>
</div>
)
@@ -74,12 +74,12 @@ type ItemProps = {
item: Story
}
-const Item = (props: ItemProps) => (
- <li key={props.item.objectID}>
- <span><a href={props.item.url}>{props.item.title}</a></span>
- <span>{props.item.author}</span>
- <span>{props.item.num_comments}</span>
- <span>{props.item.points}</span>
+const Item = ({ item }: ItemProps) => (
+ <li key={item.objectID}>
+ <span><a href={item.url}>{item.title}</a></span>
+ <span>{item.author}</span>
+ <span>{item.num_comments}</span>
+ <span>{item.points}</span>
</li>
)
@@ -87,9 +87,9 @@ type ListProps = {
list: Story[]
}
-const List = (props: ListProps) => (
+const List = ({ list }: ListProps) => (
<ul>
- {props.list.map((item) => (
+ {list.map((item) => (
<Item key={item.objectID} item={item} />
))}
</ul>
Nested Destructuring
ネストしたオブジェクトを分割代入することもできる。
code:ts
const user = {
firstName: 'Robin',
pet: {
name: 'Pochi',
},
};
const {
firstName,
pet: {
name,
},
} = user;
console.log(firstName); //=> Robin
console.log(name); //=> Pochi
Itemまわりも書き換えてみる。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index 4cae712..def0be2 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -74,12 +74,23 @@ type ItemProps = {
item: Story
}
-const Item = ({ item }: ItemProps) => (
- <li key={item.objectID}>
- <span><a href={item.url}>{item.title}</a></span>
- <span>{item.author}</span>
- <span>{item.num_comments}</span>
- <span>{item.points}</span>
+const Item = (
+ {
+ item: {
+ title,
+ url,
+ author,
+ num_comments,
+ points,
+ objectID
+ }
+ }: ItemProps
+) => (
+ <li key={objectID}>
+ <span><a href={url}>{title}</a></span>
+ <span>{author}</span>
+ <span>{num_comments}</span>
+ <span>{points}</span>
</li>
)
Spread and Rest Operators
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index def0be2..98c9d5d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -74,18 +74,14 @@ type ItemProps = {
item: Story
}
-const Item = (
- {
- item: {
- title,
- url,
- author,
- num_comments,
- points,
- objectID
- }
- }: ItemProps
-) => (
+const Item = ({
+ title,
+ url,
+ author,
+ num_comments,
+ points,
+ objectID
+}: Story) => (
<li key={objectID}>
<span><a href={url}>{title}</a></span>
<span>{author}</span>
@@ -101,7 +97,7 @@ type ListProps = {
const List = ({ list }: ListProps) => (
<ul>
{list.map((item) => (
- <Item key={item.objectID} item={item} />
+ <Item key={item.objectID} {...item} />
))}
</ul>
)
React Side-Effects
検索文字列をローカルストレージへ保存して、リロードしても文字列を復元できるようにする。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index 5b249fa..a70415c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -31,7 +31,14 @@ const App = () => {
},
]
+ localStorage.getItem('search') || 'React'
+
+ )
+
+ React.useEffect(() => {
+ localStorage.setItem('search', searchTerm)
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value)
React Custom Hooks (Advanced)
これまで使用した React Hook
useState
useEffect
React Hookを自作してみる → useStorageState
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index a70415c..943f62e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -9,6 +9,18 @@ type Story = {
objectID: number
}
+ localStorage.getItem(key) || initialState
+ )
+
+ React.useEffect(() => {
+ localStorage.setItem(key, value)
+
+}
+
const App = () => {
console.log("Redner App !!!")
@@ -31,14 +43,7 @@ const App = () => {
},
]
- localStorage.getItem('search') || 'React'
-
- )
-
- React.useEffect(() => {
- localStorage.setItem('search', searchTerm)
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value)
React Fragments
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index 943f62e..c02703b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -70,7 +70,7 @@ type SearchProps = {
const Search = ({search, onSearch}: SearchProps) => {
return (
- <div>
+ <>
<label htmlFor="search">Search: </label>
<input
id="search"
@@ -78,7 +78,7 @@ const Search = ({search, onSearch}: SearchProps) => {
value={search}
onChange={onSearch}
/>
- </div>
+ </>
)
}
Reusable React Component
検索フィールドをちょっと抽象化してみる。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index c02703b..f780ccd 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -56,27 +56,35 @@ const App = () => {
<h1>
My Hacker Stories
</h1>
- <Search search={searchTerm} onSearch={handleSearch}/>
+ <InputWithLabel
+ id="search"
+ label="Search"
+ type="text"
+ value={searchTerm}
+ onInputChange={handleSearch}
+ />
<hr />
<List list={searchStories} />
</div>
)
}
-type SearchProps = {
- search: string
- onSearch: (event: React.ChangeEvent<HTMLInputElement>) => void
+type InputWithLabelProps = {
+ id: string
+ label: string
+ type: string
+ value: string
+ onInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void
}
-
-const Search = ({search, onSearch}: SearchProps) => {
+const InputWithLabel = ({id, label, type, value, onInputChange}: InputWithLabelProps) => {
return (
<>
- <label htmlFor="search">Search: </label>
+ <label htmlFor="{id}">Search: </label>
<input
- id="search"
- type="text"
- value={search}
- onChange={onSearch}
+ id={id}
+ type={type}
+ value={value}
+ onChange={onInputChange}
/>
</>
)
React Component Composition
タグ内の子要素を children で受け取れる。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index f780ccd..377a869 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -62,7 +62,9 @@ const App = () => {
type="text"
value={searchTerm}
onInputChange={handleSearch}
- />
+ >
+ <strong>Search:</strong>
+ </InputWithLabel>
<hr />
<List list={searchStories} />
</div>
@@ -74,12 +76,14 @@ type InputWithLabelProps = {
label: string
type: string
value: string
- onInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void
+ onInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void,
+ children: React.ReactNode
}
-const InputWithLabel = ({id, label, type, value, onInputChange}: InputWithLabelProps) => {
+const InputWithLabel = ({id, label, type, value, onInputChange, children}: InputWithLabelProps) => {
return (
<>
- <label htmlFor="{id}">Search: </label>
+ <label htmlFor="{id}">{children}</label>
+
<input
id={id}
type={type}
Imperative React
useRef を使って宣言的にどの要素にフォーカスするかを書いてみる。
どのコンポーネントにフォーカスが当たるかをコンポーネントの呼び出し側で宣言してやり、コンポーネントの内部でフォーカスが宣言されていたらフォーカス当てる作業を行う。
各コンポーネントの中で呼ばれる useEffect のハンドラの中で input 要素を参照するために useRef を利用している。const inputRef = React.useRef() で定義した inputRef を input要素のref属性として渡すと、useEffectのハンドラの中でinput要素を参照することができるみたい。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index d53e605..090fa99 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -61,6 +61,7 @@ const App = () => {
label="Search"
type="text"
value={searchTerm}
+ isFocused
onInputChange={handleSearch}
<strong>Search:</strong>
@@ -76,15 +77,25 @@ type InputWithLabelProps = {
label: string
type: string
value: string
+ isFocused: boolean
onInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void,
children: React.ReactNode
}
-const InputWithLabel = ({id, label, type, value, onInputChange, children}: InputWithLabelProps) => {
+const InputWithLabel = ({id, label, type, value, isFocused, onInputChange, children}: InputWithLabelProps) => {
+ const inputRef = React.useRef<HTMLInputElement>(null)
+
+ React.useEffect(() => {
+ if (isFocused && inputRef.current) {
+ inputRef.current.focus()
+ }
+
return (
<>
<label htmlFor="{id}">{children}</label>
<input
+ ref={inputRef}
id={id}
type={type}
value={value}
React.useRef
React.useRef で定義したMutableRefObject を参照したいHTML要素のref属性として渡すことで、MutableRefObject経由でHTML要素を参照することができる。
Inline Handler in JSX
選択した要素を削除する。ボタンにイベントを束縛するタイミングで引数を以下のようにセットしてあげるのがポイント。
code:tsx
<button onClick={() => { onRemoveItem(item) }}>Remove</button>
コードの全体はこんな感じ。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index 090fa99..4f3a462 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -24,7 +24,7 @@ const useStorageState = (key: string, initialState: string): [string, (newValue:
const App = () => {
console.log("Redner App !!!")
- const stories: Story[] = [
{
title: 'React',
@@ -41,7 +41,7 @@ const App = () => {
points: 5,
objectID: 1,
},
- ]
+ ])
@@ -49,6 +49,11 @@ const App = () => {
setSearchTerm(event.target.value)
}
+ const handleRemoveStory = (story: Story) => {
+ const newStories = stories.filter((e) => e.objectID !== story.objectID)
+ setStories(newStories)
+ }
+
const searchStories = stories.filter((story) => { return story.title.toLowerCase().includes(searchTerm.toLowerCase()) })
return (
@@ -67,7 +72,7 @@ const App = () => {
<strong>Search:</strong>
</InputWithLabel>
<hr />
- <List list={searchStories} />
+ <List list={searchStories} onRemoveItem={handleRemoveStory} />
</div>
)
}
@@ -107,32 +112,37 @@ const InputWithLabel = ({id, label, type, value, isFocused, onInputChange, child
type ItemProps = {
item: Story
+ onRemoveItem: (item: Story) => void
}
const Item = ({
- title,
- url,
- author,
- num_comments,
- points,
- objectID
-}: Story) => (
- <li key={objectID}>
- <span><a href={url}>{title}</a></span>
- <span>{author}</span>
- <span>{num_comments}</span>
- <span>{points}</span>
+ item,
+ onRemoveItem
+}: ItemProps) => (
+ <li key={item.objectID}>
+ <span><a href={item.url}>{item.title}</a></span>
+ <span>{item.author}</span>
+ <span>{item.num_comments}</span>
+ <span>{item.points}</span>
+ <span>
+ <button onClick={() => { onRemoveItem(item) }}>Remove</button>
+ </span>
</li>
)
type ListProps = {
list: Story[]
+ onRemoveItem: (item: Story) => void
}
-const List = ({ list }: ListProps) => (
+const List = ({ list, onRt {...item} />
+ <Item
+ key={item.objectID}
+ item={item}
+ onRemoveItem={onRemoveItem}
+ />
))}
</ul>
)
React Asynchronous Data
リモートからデータを取得する場合はPromiseを使ってデータを取得する必要が出てくる。今回はリモートからデータを取得する代わりにPromiseとsetTimeoutを使用して非同期に値を返している。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index 83f838c..d65798d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -41,9 +41,27 @@ const App = () => {
},
]
+ type AsyncStoriesResponse = { data: { stories: Story[] } }
+ const getAsyncStories = () => {
+ return new Promise<AsyncStoriesResponse>((resolve) => {
+ setTimeout(
+ () => {
+ resolve({ data: { stories: initialStories } })
+ },
+ 2000
+ )
+ })
+ }
+
+ React.useEffect(() => {
+ getAsyncStories().then((result) => {
+ setStories(result.data.stories)
+ })
+ }, [])
+
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value)
}
React Conditional Rendering
ロード中の表記とエラー時の表記を追加。三項演算子や && を使って出し分けを行うと良い。
https://gyazo.com/e0fd07df8c78052db9fc8ce48fee152c
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index d65798d..61caca9 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -43,10 +43,13 @@ const App = () => {
type AsyncStoriesResponse = { data: { stories: Story[] } }
const getAsyncStories = () => {
- return new Promise<AsyncStoriesResponse>((resolve) => {
+ return new Promise<AsyncStoriesResponse>((resolve, reject) => {
setTimeout(
() => {
resolve({ data: { stories: initialStories } })
+
+ // NOTE: エラーを投げる時はこんな感じ
+ // reject(new Error("oh!!"))
},
2000
)
@@ -54,11 +57,19 @@ const App = () => {
}
React.useEffect(() => {
+ setIsLoading(true)
+
getAsyncStories().then((result) => {
setStories(result.data.stories)
+ setIsLoading(false)
+ }).catch(() => {
+ setIsError(true)
+ setIsLoading(false)
})
}, [])
@@ -89,7 +100,17 @@ const App = () => {
<strong>Search:</strong>
</InputWithLabel>
<hr />
- <List list={searchStories} onRemoveItem={handleRemoveStory} />
+
+ {isError && <p>Something went wrong ...</p>}
+
+ {
+ isLoading ? (
+ <p>Loading...</p>
+ ) : (
+ <List list={searchStories} onRemoveItem={handleRemoveStory} />
+ )
+ }
+
</div>
)
}
React Advanced State
useReducerHook が出てきた。
JavaScript Reducer
React公式のuseReducerの説明
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index 61caca9..c5ba56d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -56,7 +56,30 @@ const App = () => {
})
}
+ type StoriesReducerAction = {
+ type: 'SET_STORIES',
+ payload: Story[]
+ } | {
+ type: 'REMOVE_STORY',
+ payload: Story
+ }
+
+ const storiesReducer = (state: Story[], action: StoriesReducerAction) => {
+ switch (action.type) {
+ case 'SET_STORIES':
+ return action.payload
+ case 'REMOVE_STORY':
+ return state.filter((e) => e.objectID !== action.payload.objectID)
+ default:
+ throw new Error()
+ }
+ }
+
+ storiesReducer,
+ []
+ )
+
@@ -65,7 +88,10 @@ const App = () => {
setIsLoading(true)
getAsyncStories().then((result) => {
- setStories(result.data.stories)
+ dispatchStories({
+ type: 'SET_STORIES',
+ payload: result.data.stories
+ })
setIsLoading(false)
}).catch(() => {
setIsError(true)
@@ -78,8 +104,10 @@ const App = () => {
}
const handleRemoveStory = (story: Story) => {
- const newStories = stories.filter((e) => e.objectID !== story.objectID)
- setStories(newStories)
+ dispatchStories({
+ type: 'REMOVE_STORY',
+ payload: story
+ })
}
const searchStories = stories.filter((story) => { return story.title.toLowerCase().includes(searchTerm.toLowerCase()) })
React Impossible States
複数の useState をひとつの useReducer にまとめる。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index c5ba56d..37c8f25 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -56,20 +56,50 @@ const App = () => {
})
}
+ type StoriesReducerState = {
+ data: Story[],
+ isLoading: boolean,
+ isError: boolean,
+ }
+
type StoriesReducerAction = {
- type: 'SET_STORIES',
+ type: 'STORIES_FETCH_INIT',
+ } | {
+ type: 'STORIES_FETCH_SUCCESS',
payload: Story[]
+ } | {
+ type: 'STORIES_FETCH_FAILURE',
} | {
type: 'REMOVE_STORY',
payload: Story
}
- const storiesReducer = (state: Story[], action: StoriesReducerAction) => {
+ const storiesReducer = (state: StoriesReducerState, action: StoriesReducerAction) => {
switch (action.type) {
- case 'SET_STORIES':
- return action.payload
+ case 'STORIES_FETCH_INIT':
+ return {
+ ...state,
+ isLoading: true,
+ isError: false,
+ }
+ case 'STORIES_FETCH_SUCCESS':
+ return {
+ ...state,
+ data: action.payload,
+ isLoading: false,
+ isError: false,
+ }
+ case 'STORIES_FETCH_FAILURE':
+ return {
+ ...state,
+ isLoading: false,
+ isError: true,
+ }
case 'REMOVE_STORY':
- return state.filter((e) => e.objectID !== action.payload.objectID)
+ return {
+ ...state,
+ data: state.data.filter((e) => e.objectID !== action.payload.objectID),
+ }
default:
throw new Error()
}
@@ -77,25 +107,21 @@ const App = () => {
storiesReducer,
- []
+ { data: [], isLoading: false, isError: false }
)
React.useEffect(() => {
- setIsLoading(true)
+ dispatchStories({ type: 'STORIES_FETCH_INIT' })
getAsyncStories().then((result) => {
dispatchStories({
- type: 'SET_STORIES',
- payload: result.data.stories
+ type: 'STORIES_FETCH_SUCCESS',
+ payload: result.data.stories,
})
- setIsLoading(false)
}).catch(() => {
- setIsError(true)
- setIsLoading(false)
+ dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
})
}, [])
@@ -104,13 +130,10 @@ const App = () => {
}
const handleRemoveStory = (story: Story) => {
- dispatchStories({
- type: 'REMOVE_STORY',
- payload: story
- })
+ dispatchStories({type: 'REMOVE_STORY', payload: story })
}
- const searchStories = stories.filter((story) => { return story.title.toLowerCase().includes(searchTerm.toLowerCase()) })
+ const searchStories = stories.data.filter((story) => { return story.title.toLowerCase().includes(searchTerm.toLowerCase()) })
return (
<div>
@@ -129,10 +152,10 @@ const App = () => {
</InputWithLabel>
<hr />
- {isError && <p>Something went wrong ...</p>}
+ {stories.isError && <p>Something went wrong ...</p>}
{
- isLoading ? (
+ stories.isLoading ? (
<p>Loading...</p>
) : (
<List list={searchStories} onRemoveItem={handleRemoveStory} />
Data Fetching with React
Hacker News API
Using Fetch API
Chrome のコンソールでFetch APIを叩いてみる
code:js
ダミーデータの代わりにHaker Newsから取得したデータを表示する。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index 37c8f25..13db786 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -113,16 +113,20 @@ const App = () => {
React.useEffect(() => {
+
dispatchStories({ type: 'STORIES_FETCH_INIT' })
- getAsyncStories().then((result) => {
- dispatchStories({
- type: 'STORIES_FETCH_SUCCESS',
- payload: result.data.stories,
+ fetch(${API_ENDPOINT}react)
+ .then((response) => response.json())
+ .then((result) => {
+ dispatchStories({type: 'STORIES_FETCH_SUCCESS', payload: result.hits})
+ console.log(result)
+ })
+ .catch((err) => {
+ dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
+ console.error(err)
})
- }).catch(() => {
- dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
- })
}, [])
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
こんな感じ。
https://gyazo.com/9c4be10eed5e2f473f6a61a6e55e9730
不要になったコードを削除。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index 13db786..8382d3f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -22,40 +22,6 @@ const useStorageState = (key: string, initialState: string): [string, (newValue:
}
const App = () => {
- const initialStories = [
- {
- title: 'React',
- author: 'Jordan Walke',
- num_comments: 3,
- points: 4,
- objectID: 0,
- },
- {
- title: 'Redux',
- author: 'Dan Abramov, Andrew Clark',
- num_comments: 2,
- points: 5,
- objectID: 1,
- },
- ]
-
- type AsyncStoriesResponse = { data: { stories: Story[] } }
- const getAsyncStories = () => {
- return new Promise<AsyncStoriesResponse>((resolve, reject) => {
- setTimeout(
- () => {
- resolve({ data: { stories: initialStories } })
-
- // NOTE: エラーを投げる時はこんな感じ
- // reject(new Error("oh!!"))
- },
- 2000
- )
- })
- }
-
type StoriesReducerState = {
data: Story[],
isLoading: boolean,
Data Re-Fetching with React
検索文字列を変更したら再検索しに行くようにする。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index 8382d3f..b4c2ba7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -79,11 +79,13 @@ const App = () => {
React.useEffect(() => {
+ if (!searchTerm) return
+
dispatchStories({ type: 'STORIES_FETCH_INIT' })
- fetch(${API_ENDPOINT}react)
+ fetch(${API_ENDPOINT}${searchTerm})
.then((response) => response.json())
.then((result) => {
dispatchStories({type: 'STORIES_FETCH_SUCCESS', payload: result.hits})
@@ -93,7 +95,7 @@ const App = () => {
dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
console.error(err)
})
- }, [])
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value)
@@ -103,8 +105,6 @@ const App = () => {
dispatchStories({type: 'REMOVE_STORY', payload: story })
}
- const searchStories = stories.data.filter((story) => { return story.title.toLowerCase().includes(searchTerm.toLowerCase()) })
-
return (
<div>
<h1>
@@ -128,7 +128,7 @@ const App = () => {
stories.isLoading ? (
<p>Loading...</p>
) : (
- <List list={searchStories} onRemoveItem={handleRemoveStory} />
+ <List list={stories.data} onRemoveItem={handleRemoveStory} />
)
}
Memoized Functions in React (Advanced)
useCallback でメモ化。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index b4c2ba7..43f124d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -78,7 +78,7 @@ const App = () => {
- React.useEffect(() => {
+ const handleFetchStories = React.useCallback(() => {
if (!searchTerm) return
@@ -88,6 +88,7 @@ const App = () => {
fetch(${API_ENDPOINT}${searchTerm})
.then((response) => response.json())
.then((result) => {
dispatchStories({type: 'STORIES_FETCH_SUCCESS', payload: result.hits})
console.log(result)
})
@@ -97,6 +98,10 @@ const App = () => {
})
+ React.useEffect(() => {
+ handleFetchStories()
+
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value)
}
Explicit Data Fetching with React
Submit ボタンを押した時のみ検索するようにする。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index d2e02a9..ca47087 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -78,14 +78,15 @@ const App = () => {
+ const url, setUrl = React.useState(${API_ENDPOINT}${searchTerm}) +
const handleFetchStories = React.useCallback(() => {
if (!searchTerm) return
-
dispatchStories({ type: 'STORIES_FETCH_INIT' })
- fetch(${API_ENDPOINT}${searchTerm})
+ fetch(url)
.then((response) => response.json())
.then((result) => {
dispatchStories({type: 'STORIES_FETCH_SUCCESS', payload: result.hits})
@@ -95,7 +96,7 @@ const App = () => {
dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
console.error(err)
})
React.useEffect(() => {
handleFetchStories()
@@ -105,6 +106,14 @@ const App = () => {
setSearchTerm(event.target.value)
}
+ const handleSearchInput = (event: React.ChangeEvent<HTMLInputElement>) => {
+ setSearchTerm(event.target.value)
+ }
+
+ const handleSearchSubmit = () => {
+ setUrl(${API_ENDPOINT}${searchTerm})
+ }
+
const handleRemoveStory = (story: Story) => {
dispatchStories({type: 'REMOVE_STORY', payload: story })
}
@@ -120,10 +129,17 @@ const App = () => {
type="text"
value={searchTerm}
isFocused
- onInputChange={handleSearch}
+ onInputChange={handleSearchInput}
<strong>Search:</strong>
</InputWithLabel>
+ <button
+ type="button"
+ disabled={!searchTerm}
+ onClick={handleSearchSubmit}
+ >
+ Submit
+ </button>
<hr />
{stories.isError && <p>Something went wrong ...</p>}
Third-Party Libraries in React
fetch API を axios で置き換える。
code:sh
npm install axios
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index ca47087..3a7e48e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,5 @@
import * as React from 'react'
+import axios from 'axios'
type Story = {
title: string
@@ -86,10 +87,10 @@ const App = () => {
dispatchStories({ type: 'STORIES_FETCH_INIT' })
- fetch(url)
- .then((response) => response.json())
+ axios
+ .get(url)
.then((result) => {
- dispatchStories({type: 'STORIES_FETCH_SUCCESS', payload: result.hits})
+ dispatchStories({type: 'STORIES_FETCH_SUCCESS', payload: result.data.hits})
console.log(result)
})
.catch((err) => {
Async/Await in React
async function
then/catch の代わりに async/await を使ってみる。例外のハンドリングは try を使う。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index 3a7e48e..dae97e1 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -82,21 +82,18 @@ const App = () => {
const url, setUrl = React.useState(${API_ENDPOINT}${searchTerm}) - const handleFetchStories = React.useCallback(() => {
+ const handleFetchStories = React.useCallback(async () => {
if (!searchTerm) return
dispatchStories({ type: 'STORIES_FETCH_INIT' })
- axios
- .get(url)
- .then((result) => {
- dispatchStories({type: 'STORIES_FETCH_SUCCESS', payload: result.data.hits})
- console.log(result)
- })
- .catch((err) => {
- dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
- console.error(err)
- })
+ try {
+ const result = await axios.get(url)
+ dispatchStories({type: 'STORIES_FETCH_SUCCESS', payload: result.data.hits})
+ } catch(err) {
+ dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
+ console.error(err)
+ }
React.useEffect(() => {
Forms in React
form を使って書き直してみる。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index dae97e1..9556640 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -108,8 +108,9 @@ const App = () => {
setSearchTerm(event.target.value)
}
- const handleSearchSubmit = () => {
+ const handleSearchSubmit = (event: SubmitEvent) => {
setUrl(${API_ENDPOINT}${searchTerm})
+ event.preventDefault()
}
const handleRemoveStory = (story: Story) => {
@@ -121,23 +122,25 @@ const App = () => {
<h1>
My Hacker Stories
</h1>
- <InputWithLabel
- id="search"
- label="Search"
- type="text"
- value={searchTerm}
- isFocused
- onInputChange={handleSearchInput}
- >
- <strong>Search:</strong>
- </InputWithLabel>
- <button
- type="button"
- disabled={!searchTerm}
- onClick={handleSearchSubmit}
- >
- Submit
- </button>
+ <form onSubmit={handleSearchSubmit}>
+ <InputWithLabel
+ id="search"
+ label="Search"
+ type="text"
+ value={searchTerm}
+ isFocused
+ onInputChange={handleSearchInput}
+ >
+ <strong>Search:</strong>
+ </InputWithLabel>
+ <button
+ type="submit"
+ disabled={!searchTerm}
+ >
+ Submit
+ </button>
+ </form>
+
<hr />
{stories.isError && <p>Something went wrong ...</p>}
form を SearchForm コンポーネントとして切り出す。
code:diff
diff --git a/src/App.tsx b/src/App.tsx
index dae97e1..4e67c04 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -108,8 +108,9 @@ const App = () => {
setSearchTerm(event.target.value)
}
- const handleSearchSubmit = () => {
+ const handleSearchSubmit = (event: React.FormEvent) => {
setUrl(${API_ENDPOINT}${searchTerm})
+ event.preventDefault()
}
const handleRemoveStory = (story: Story) => {
@@ -121,36 +122,53 @@ const App = () => {
<h1>
My Hacker Stories
</h1>
+ <SearchForm
+ searchTerm={searchTerm}
+ onSearchInput={handleSearchInput}
+ onSearchSubmit={handleSearchSubmit}
+ />
+
+ <hr />
+
+ {stories.isError && <p>Something went wrong ...</p>}
+
+ {
+ stories.isLoading ? (
+ <p>Loading...</p>
+ ) : (
+ <List list={stories.data} onRemoveItem={handleRemoveStory} />
+ )
+ }
+
+ </div>
+ )
+}
+
+type SearchFormProps = {
+ searchTerm: string
+ onSearchInput: (event: React.ChangeEvent<HTMLInputElement>) => void
+ onSearchSubmit: (event: React.FormEvent) => void
+}
+const SearchForm = ({searchTerm, onSearchInput, onSearchSubmit}: SearchFormProps) => {
+ return (
+ <form onSubmit={onSearchSubmit}>
<InputWithLabel
id="search"
label="Search"
type="text"
value={searchTerm}
isFocused
- onInputChange={handleSearchInput}
+ onInputChange={onSearchInput}
<strong>Search:</strong>
</InputWithLabel>
<button
- type="button"
+ type="submit"
disabled={!searchTerm}
- onClick={handleSearchSubmit}
Submit
</button>
- <hr />
-
- {stories.isError && <p>Something went wrong ...</p>}
-
- {
- stories.isLoading ? (
- <p>Loading...</p>
- ) : (
- <List list={stories.data} onRemoveItem={handleRemoveStory} />
- )
- }
-
- </div>
+ </form>
)
}